package com.btb.core.contract;

import static org.web3j.utils.RevertReasonExtractor.extractRevertReason;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import org.web3j.abi.EventEncoder;
import org.web3j.abi.EventValues;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Event;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.RemoteFunctionCall;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthCall;
import org.web3j.protocol.core.methods.response.EthEstimateGas;
import org.web3j.protocol.core.methods.response.EthGasPrice;
import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
import org.web3j.protocol.core.methods.response.Log;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.exceptions.TransactionException;
import org.web3j.tx.Contract.EventValuesWithLog;
import org.web3j.tx.ManagedTransaction;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.TransactionManager;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class BaseContractFunction extends ManagedTransaction {

	protected String contractAddress;

	protected Credentials credentials;

	protected TransactionManager transactionManager;

	private BaseContractFunction(String contractAddress, Web3j web3j, TransactionManager transactionManager) {
		super(web3j, transactionManager);
		this.contractAddress = contractAddress;
		this.transactionManager = transactionManager;
	}

	public static EventValues staticExtractEventParameters(Event event, Log log) {
		final List<String> topics = log.getTopics();
		String encodedEventSignature = EventEncoder.encode(event);
		if (topics == null || topics.size() == 0 || !topics.get(0).equals(encodedEventSignature)) {
			return null;
		}

		List<Type> indexedValues = new ArrayList<>();
		List<Type> nonIndexedValues = FunctionReturnDecoder.decode(log.getData(), event.getNonIndexedParameters());

		List<TypeReference<Type>> indexedParameters = event.getIndexedParameters();
		for (int i = 0; i < indexedParameters.size(); i++) {
			Type value = FunctionReturnDecoder.decodeIndexedValue(topics.get(i + 1), indexedParameters.get(i));
			indexedValues.add(value);
		}
		return new EventValues(indexedValues, nonIndexedValues);
	}

	protected static EventValuesWithLog staticExtractEventParametersWithLog(Event event, Log log) {
		final EventValues eventValues = staticExtractEventParameters(event, log);
		return (eventValues == null) ? null : new EventValuesWithLog(eventValues, log);
	}

	protected EventValuesWithLog extractEventParametersWithLog(Event event, Log log) {
		return staticExtractEventParametersWithLog(event, log);
	}

	protected List<EventValuesWithLog> extractEventParametersWithLog(Event event,
			TransactionReceipt transactionReceipt) {
		return transactionReceipt.getLogs().stream().map(log -> extractEventParametersWithLog(event, log))
				.filter(Objects::nonNull).collect(Collectors.toList());
	}

	public BaseContractFunction(String contractAddress, Web3j web3j, Credentials credentials) {
		this(contractAddress, web3j, new RawTransactionManager(web3j, credentials));
		this.credentials = Objects.requireNonNull(credentials, "credentials is null");
	}

	public static BigInteger gasLimit(Web3j web3j, String from, String to, Function function) {
		Transaction transaction = Transaction.createEthCallTransaction(from, to, FunctionEncoder.encode(function));
		try {
			EthEstimateGas ethEstimateGas = web3j.ethEstimateGas(transaction).send();
			if (ethEstimateGas.hasError()) {
				throw new RuntimeException(ethEstimateGas.getError().getMessage());
			}
			return ethEstimateGas.getAmountUsed();
		} catch (IOException e) {
			throw new RuntimeException("net error");
		}
	}

	public BigInteger gasLimit(Function function) {
		Transaction transaction = Transaction.createEthCallTransaction(credentials.getAddress(), contractAddress,
				FunctionEncoder.encode(function));
		try {
			EthEstimateGas ethEstimateGas = web3j.ethEstimateGas(transaction).send();
			if (ethEstimateGas.hasError()) {
				throw new RuntimeException(ethEstimateGas.getError().getMessage());
			}
			return ethEstimateGas.getAmountUsed();
		} catch (IOException e) {
			throw new RuntimeException("net error");
		}
	}

	public BigInteger getNonce(String addr) {
		try {
			EthGetTransactionCount getNonce = web3j.ethGetTransactionCount(addr, DefaultBlockParameterName.PENDING)
					.send();
			if (getNonce == null) {
				throw new RuntimeException("net error");
			}
			return getNonce.getTransactionCount();
		} catch (IOException e) {
			throw new RuntimeException("net error");
		}
	}

	public BigInteger gasLimit(final String data, final BigInteger value, BigInteger gasPrice) {
		Transaction transaction = Transaction.createFunctionCallTransaction(credentials.getAddress(), null, gasPrice, null, contractAddress, value, data);
		try {
			EthEstimateGas ethEstimateGas = web3j.ethEstimateGas(transaction).send();
			if (ethEstimateGas.hasError()) {
				throw new RuntimeException(ethEstimateGas.getError().getMessage());
			}
			return ethEstimateGas.getAmountUsed();
		} catch (IOException e) {
			log.error("net error:{}", e.getMessage(), e);
			throw new RuntimeException("net error")  ;
		}
	}

	public BigInteger gasPrice() {
		EthGasPrice ethGasPrice = null;
		try {
			ethGasPrice = web3j.ethGasPrice().send();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return Optional.ofNullable(ethGasPrice).map(EthGasPrice::getGasPrice)
				.orElseThrow(() -> new IllegalStateException(""));
	}

	protected RemoteFunctionCall<TransactionReceipt> executeRemoteCallTransaction(Function function,
			BigInteger weiValue) {
		return new RemoteFunctionCall<>(function, () -> executeTransaction(function, weiValue));
	}

	protected RemoteFunctionCall<TransactionReceipt> executeRemoteCallTransaction(Function function) {
		return new RemoteFunctionCall<>(function, () -> executeTransaction(function, BigInteger.ZERO));
	}

	private TransactionReceipt executeTransaction(Function function, BigInteger weiValue)
			throws IOException, TransactionException {
		return executeTransaction(FunctionEncoder.encode(function), weiValue, function.getName());
	}

	TransactionReceipt executeTransaction(String data, BigInteger weiValue, String funcName)
			throws TransactionException, IOException {
		return executeTransaction(data, weiValue, funcName, false);
	}

	TransactionReceipt executeTransaction(String data, BigInteger weiValue, String funcName, boolean constructor)
			throws TransactionException, IOException {
		BigInteger gasPrice = gasPrice();
		// 受保护的nonce方法
		BigInteger gasLimit = gasLimit(data, weiValue, gasPrice);
		TransactionReceipt receipt = send(contractAddress, data, weiValue, gasPrice, gasLimit, constructor);
		if (!receipt.isStatusOK()) {
			throw new TransactionException(String.format(
					"Transaction %s has failed with status: %s. " + "Gas used: %s. " + "Revert reason: '%s'.",
					receipt.getTransactionHash(), receipt.getStatus(),
					receipt.getGasUsedRaw() != null ? receipt.getGasUsed().toString() : "unknown",
					extractRevertReason(receipt, data, web3j, true)), receipt);
		}
		return receipt;
	}
    public static class EventValuesWithLog {
        private final EventValues eventValues;
        private final Log log;

        private EventValuesWithLog(EventValues eventValues, Log log) {
            this.eventValues = eventValues;
            this.log = log;
        }

        public List<Type> getIndexedValues() {
            return eventValues.getIndexedValues();
        }

        public List<Type> getNonIndexedValues() {
            return eventValues.getNonIndexedValues();
        }

        public Log getLog() {
            return log;
        }
    }
	@SuppressWarnings("rawtypes")
	public List<Type> readOnlyFunction(Function function) {
		List<Type> response = null;
		String encodeFunction = FunctionEncoder.encode(function);
		try {
			EthCall call = web3j.ethCall(Transaction.createEthCallTransaction(null, contractAddress, encodeFunction),
					DefaultBlockParameterName.LATEST).sendAsync().get();
			response = FunctionReturnDecoder.decode(call.getValue(), function.getOutputParameters());
		} catch (InterruptedException | ExecutionException e) {
			log.error("readOnlyFunction {}, err:{}", function.getName(), e.getMessage(), e);
		}
		return response;
	}

}
